製作圖片讀取進度條與百分比
(效果預覽)
Steps Zero-核心鉤子 useOnLoadImages
傳入參數
名稱 Name | 型別 Type | 屬性 Attributes | 預設 Default | 描述 Description |
---|---|---|---|---|
ref | RefObject<HTMLElement> | 傳入想要查找 img 的外層元素 |
返回
一個 obj 物件
名稱 Name | 型別 Type | 屬性 Attributes | 預設 Default | 描述 Description |
---|---|---|---|---|
obj.status | boolean | img是否全部讀取完畢 | ||
obj.loadedCount | number | 目前讀取成功的img數量 |
程式碼
import { useState, useEffect, RefObject } from "react";
// 定義 useOnLoadImages 需要的回傳型別
interface UseOnLoadImages {
status: boolean;
loadedCount: number;
}
// 自訂 Hook: useOnLoadImages
const useOnLoadImages = (ref: RefObject<HTMLElement>): UseOnLoadImages => {
// 使用 useState 定義兩個狀態變數
const [status, setStatus] = useState(false);
const [loadedCount, setLoadedCount] = useState(0);
useEffect(() => {
// 定義一個函式,用於更新圖片載入狀態
const updateStatus = (images: HTMLImageElement[]): void => {
// 透過 map 函式檢查每個圖片是否已經載入完成
const loadedArr = images.map((image) => image.complete);
// 計算已載入完成的圖片數量
setLoadedCount(loadedArr.filter((complete) => complete).length);
// 檢查所有圖片是否都已載入完成
setStatus(loadedArr.every((item) => item));
};
// 檢查 ref 是否存在且非空
if (ref?.current === null) return;
// 取得所有在 ref 元素下的圖片元素
const imagesLoaded = Array.from(ref.current.querySelectorAll("img"));
// 若沒有圖片元素,直接將狀態設為已載入完成並返回
if (imagesLoaded.length === 0) {
setStatus(true);
return;
}
// 遍歷每個圖片元素,綁定 load 和 error 事件處理函式
imagesLoaded.forEach((image) => {
// 每次圖片載入完成或發生錯誤時,更新載入狀態
const loadedArr = imagesLoaded.map((chache) => chache.complete);
if (loadedArr.every((item) => item)) {
setStatus(true);
return;
}
image.addEventListener("load", () => updateStatus(imagesLoaded));
image.addEventListener("error", () => updateStatus(imagesLoaded));
});
});
// 返回狀態變數
return { status, loadedCount };
};
export default useOnLoadImages;
使用這個 Hook 追蹤指定元素下圖片的載入狀態。在 Hook 內部,透過 useState
定義了兩個狀態變數:
status(全部圖片是否載入完成)
loadedCount(目前成功載入多少圖片)
接著使用 useEffect
監聽指定元素下圖片載入的事件。
在 useEffect
內部,先檢查指定的元素是否存在,若不存在則直接返回。接著取得指定元素下的所有圖片元素,若沒有圖片元素則直接將載入狀態設為已完成。如果有圖片元素,則遍歷每個圖片元素,綁定 load
和 error
事件的處理函式,以便在圖片載入完成或發生錯誤時更新載入狀態。
最後,將 status
和 loadedCount
返回作為 Hook 的結果。這樣使用該 Hook 的組件就可以獲取到圖片載入的狀態和已載入完成的圖片數量。
Steps One-使用方式 1,百分比範例
以製作百分比為例,最簡單的方式,可以直接搭配 gsap.utils.mapRange
來將 loadedCount
映射成百分比
關於 mapRange 使用方式可參考我的另一篇筆記,
基本寫法
import useOnLoadImages from "./hooks/useOnLoadImages";
import { gsap } from "gsap";
const data = [
"https://i.imgur.com/5UmqIwL.jpg",
"https://i.imgur.com/mJ2RYpY.jpg",
"https://i.imgur.com/oFOt3CA.jpg",
"https://i.imgur.com/fD9NgFo.png",
"https://i.imgur.com/mrlA7SC.png",
"https://i.imgur.com/3qJXBPV.png",
"https://i.imgur.com/DeF14KL.jpg",
];
function App(){
const { status, loadedCount } = useOnLoadImages(imgWrapperRef);
return (
<>
<p className="mb-10 text-center font-dela text-8xl text-yellow-400">
{Math.round(gsap.utils.mapRange(0, 7, 0, 100, loadedCount))}%
</p>
<ul ref={imgWrapperRef} className="mx-auto max-w-2xl">
{data.map((img) => (
<li key={img}>
<img src={img} alt="" />
</li>
))}
</ul>
</>
)
}
這裡的參數 7 是假定我已經知道圖片數量有 7 個
gsap.utils.mapRange(0, 7, 0, 100, loadedCount)
串接 api 時寫法
在串接 api 時,我們總是無法提前知道圖片有幾張,因此我們可以使用 useEffect 或其他框架所提供 onMounted 的鉤子來抓取 img 數量
import React, { useEffect, useRef, useState } from "react";
import useOnLoadImages from "./hooks/useOnLoadImages";
import { gsap } from "gsap";
const data = [
"https://i.imgur.com/5UmqIwL.jpg",
"https://i.imgur.com/mJ2RYpY.jpg",
"https://i.imgur.com/oFOt3CA.jpg",
"https://i.imgur.com/fD9NgFo.png",
"https://i.imgur.com/mrlA7SC.png",
"https://i.imgur.com/3qJXBPV.png",
"https://i.imgur.com/DeF14KL.jpg",
];
function App(){
const imgWrapperRef = useRef<HTMLUListElement>(null);
const [loadedPercent, setLoadedPercent] = useState(0);
const { status, loadedCount } = useOnLoadImages(imgWrapperRef);
useEffect(() => {
if (imgWrapperRef.current === null) return;
const imgCounts = imgWrapperRef.current.querySelectorAll("img").length;
setLoadedPercent(Math.round(gsap.utils.mapRange(0, imgCounts, 0, 100, loadedCount))
);
},[])
return (
<>
<p className="mb-10 text-center font-dela text-8xl text-yellow-400">
{loadedPercent}%
</p>
<ul ref={imgWrapperRef} className="mx-auto max-w-2xl">
{data.map((img) => (
<li key={img}>
<img src={img} alt="" />
</li>
))}
</ul>
</>
)
}
Steps Two - 使用方式 2,讀取進度條
1. 宣告下列 ref 用於控制進度條動畫
/* mainCtx 用來作為 GSAP 動畫的上下文或其他與 main 元素相關的操作。*/
const mainCtx = useRef(null);
/* tlRef 是一個對 GSAP 時間軸的參考,*/
// 1. 初始化選項 { paused: true } 確保時間軸創建時將不會自動播放。
// 2. 用於控制進度條長度變化
const tlRef = useRef(gsap.timeline({ paused: true }));
/* tlCtrlRef 用於控制 tlRef 的播放進度 */
const tlCtrlRef = useRef(gsap.timeline());
/* previousProgress 儲存進度條的上一個百分比值 */
const previousProgress = useRef(0);
2. 在 useEffect 建立進度條動畫
useEffect(() => {
if (mainCtx.current === null || imgWrapperRef.current === null) return;
// 1. 獲取圖片數量
const imgCounts = imgWrapperRef.current.querySelectorAll("img").length;
// 2. 用於更新 GSAP 時間軸的進度。動畫的核心邏輯在這裡:
const progress = gsap.utils.mapRange(0, imgCounts, 0, 1, loadedCount);
const ctx = gsap.context(() => {
// 3. 建立進度條形變動畫,
// 由於此時間軸初始值在 useRef 設定為 { pause: true } ,因此不會執行動畫
tlRef.current.to("#js-progress", {
right: "0%",
});
// 4. 控制 tlRef 播放進度,使用 previousProgress 與 progress 建立過渡動畫
tlCtrlRef.current
.to(tlRef.current, {
progress: previousProgress.current,
})
.to(tlRef.current, {
progress: progress,
ease: "none",
});
previousProgress.current = progress;
}, mainCtx.current);
return () => ctx.revert();
}, [loadedCount]);
return (
<main ref={mainCtx}>
<section className="min-h-screen pt-10">
<h1 className="relative mx-auto mb-10 w-max border-2 border-black bg-white px-10 py-5 text-center font-dela text-3xl after:absolute after:left-2 after:top-2 after:-z-10 after:block after:h-full after:w-full after:bg-blue-600 after:content-['']">
React+GSAP實作圖片讀取進度條
</h1>
<p className="mb-10 text-center font-dela text-8xl text-yellow-400">
{loadedPercent}%
</p>
<div className="relative mx-auto mb-10 h-2 w-full max-w-2xl">
<div
id="js-progress"
className="absolute left-0 right-[100%] h-2 bg-yellow-500 hover:right-0"
/>
</div>
<ul ref={imgWrapperRef} className="mx-auto max-w-2xl">
{data.map((img) => (
<li key={img}>
<img src={img} alt="" />
</li>
))}
</ul>
</section>
</main>
);